You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Use robot-reported RTDE field types and improved memory copy performance - #560
Replace the hardcoded RTDE field-name/type table with types reported by the robot during recipe setup. This allows applications to use additional controller fields without updating the library's field table, provided their protocol types are supported.
Changes
Apply negotiated types throughout RTDE packages, parsing and writing; expose stored field types through DataPackage::getDataType() and their protocol names through toString(DataType).
Add RTDEClient::createInputDataPackage() for pre-typed input packages and validate outgoing packages against the negotiated recipe. Unset input fields in matching recipes are sent as typed zeros.
Preserve allocation-free send/receive paths for preallocated, matching recipes, including applying negotiated output types in place.
Harden failed handshakes, reconnect handling and fake-server lifecycle; fix output initialization in TCPServer::writeUnchecked().
Add robot-free protocol, type-validation, allocation and reconnect tests, controller-backed recipe checks, and a dedicated unit-coverage CI job.
Compatibility
Unknown fields raise RTDEInvalidKeyException during RTDEClient::init(), rather than construction. ignore_unavailable_outputs also filters unknown names.
Recipe-only packages begin untyped. Input types established by setData() are checked against the robot when sent; pre-typed input packages reject mismatches immediately.
getData() still throws std::bad_variant_access for a present field with the wrong or unset type.
std::string is no longer a DataPackage variant alternative, so using it with getData() or setData() is a compile error.
Direct parser users must configure negotiated layout/type information. Client reads into null pointers or packages with foreign recipes may allocate; reuse a package built from getOutputRecipe() for the allocation-free path.
❌ Patch coverage is 88.10484% with 59 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.36%. Comparing base (9a15f8d) to head (a808853). ⚠️ Report is 32 commits behind head on master.
✅ All tests successful. No failed tests found.
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
include/ur_client_library/rtde/data_package.h:124
std::string is not one of the RTDE protocol types, but retaining it here lets setData() change an untyped field from monostate to std::string. Such a package then passes isTyped() and serializePackage() emits a malformed variable-length payload, while getDataType() reports no type. Restrict untyped fields to the alternatives represented by DataType.
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
tests/test_rtde_allocations.cpp:52
This thread-local design excludes not only the fake server but also the client's background-reader and writer threads. Therefore background_receive_does_not_allocate does not observe the thread that parses received packages, and sending_input_data_does_not_allocate does not observe the thread that serializes and writes them; both tests can pass with allocations in the paths their names claim to cover. Instrument all client-owned threads while excluding only the server thread.
// Counting is per-thread: the fake server and, in the background-read case, the client's read
// thread run in the same process, and their allocations are none of this test's business.
thread_local std::size_t g_allocation_count = 0;
thread_local bool g_count_allocations = false;
src/rtde/rtde_parser.cpp:161
A caller can make a preallocated package “typed” with setData() before the first blocking receive, so this check does not prove that its types came from the robot. For example, a timestamp-only package set as uint64_t skips initEmpty(recipe_types_) and parses the robot's DOUBLE bytes as an integer while reporting success. Validate both the recipe and every existing field type against recipe_/recipe_types_, retyping or replacing packages that do not match.
if (!data_package->isTyped())
{
// A package built from a recipe alone doesn't know its field types yet. Applying the ones
// the robot reported doesn't allocate, so this happens right here rather than by handing
// the caller a replacement package.
tests/test_rtde_data_package.cpp:460
This test does not measure allocations, so an allocation introduced inside initEmpty(types) would still pass despite the test name and the PR's real-time guarantee. Surround this call with the allocation counter (or move this case into the allocation-test binary) and assert that its count remains zero.
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
tests/test_rtde_allocations.cpp:277
The counter is thread-local, while sendPackage() only queues data and RTDEWriter::run() performs serialization and socket writing on its own thread. Consequently this measured block cannot see allocations in the actual asynchronous send path, so sending_input_data_does_not_allocate can pass despite a send-thread regression. Add writer-thread instrumentation or a same-thread serialization allocation test.
{
AllocationCounter counter;
for (int i = 0; i < g_MEASURED_CYCLES; ++i)
{
all_sent &= input_pkg.setData("speed_slider_fraction", 0.5);
all_sent &= client_->getWriter().sendPackage(input_pkg);
}
src/rtde/rtde_parser.cpp:157
isTyped() also becomes true when the caller has populated every field with setData(), so it does not prove these are the negotiated types. A preallocated one-field timestamp package set as uint64_t, for example, skips initEmpty(recipe_types_) and parses the robot's DOUBLE bytes as UINT64. This path also never verifies field names, so an untyped same-length package with a different recipe is typed by position. Validate the recipe and always reapply the acknowledged types before parsing; replace the package only when its recipe differs.
DataPackage* data_package = dynamic_cast<DataPackage*>(result.get());
data_package->setProtocolVersion(protocol_version_);
if (!data_package->isTyped())
tests/test_rtde_data_package.cpp:460
This test never observes allocations: it only verifies that the package remains usable. The counters in test_rtde_allocations.cpp start after RTDEClient::init() and warmup, so an allocation added to initEmpty(types) would pass the suite even though no-allocation in-place typing is a central guarantee. Measure this call while allocation counting is active.
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
tests/test_rtde_allocations.cpp:184
This guard can pass while the allocation tests miss the allocations they are intended to detect. As the comment above notes, some libstdc++/musl std::allocator implementations call malloc directly; invoking ::operator new here proves only that this replacement works, while vector/string growth in the measured RTDE paths can bypass it and leave the count at zero. Validate the counter with a representative standard-container allocation and either intercept that platform's allocation path or fail/skip when it cannot be observed.
// Guards the tests below: if the counter stopped seeing allocations, they would pass vacuously.
// Call operator new directly rather than writing `new int`: a new-expression may be omitted even
// when the pointer escapes, which is what Alpine's gcc 15 does at -O2. Allocate with operator new
// rather than a container: on some libstdc++ / musl builds std::allocator uses malloc and would
// never hit the replaced operator new that the RTDE tests count.
TEST(AllocationCounterTest, counts_allocations)
{
std::size_t allocations = 0;
{
AllocationCounter counter;
g_allocation_sink = ::operator new(sizeof(int));
src/rtde/data_package.cpp:212
getDataType() does not necessarily report a robot-acknowledged type as the new API promises. On an application-created input package, setData() changes the variant from monostate to the caller's type, so this function then returns that inferred type—even when RTDEWriter::sendPackage() later rejects it because the robot reported a different type. Consumers therefore cannot tell whether this result is authoritative. Track the acknowledged type separately from the value/inferred type, or explicitly expose this as the stored value type and provide the robot-reported type through the client/writer API.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The new type-reporting contract is inconsistent with caller-established types, and reconnect tests contain synchronization and coverage defects.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
examples/rtde_writer.cpp:93
Correct the typo in this user-facing example comment: “may” should be “many.” tests/test_rtde_client_reconnect.cpp:320
These two point-in-time state checks do not establish that reconnecting stopped. During each retry the client repeatedly returns to UNINITIALIZED, so an implementation that retries forever can satisfy both assertions. Wait for the expected request count and then verify requestedProtocolVersions() remains unchanged (or expose a completion signal) to cover exhaustion rather than an incidental state between attempts.
…plate
The deprecated overload read into preallocated_data_pkg_ and then copied it without a
lock, while reconnect() retypes that package holding only reconnect_mutex_. Both the
read and the copy could race with a reconnect and return a partially retyped package.
It now copies the latest background buffer under read_mutex_ into a package it owns
and reads into that instead, so it never touches the template.
Ensure failures stop the running robot program before returning
examples/rtde_roundtrip.cpp:155
Any failure inside the loop returns before the cleanup at the end of main, so the uploaded infinite rtde_register_mirror program remains running on the robot (the same bypass occurs at the other two return 1 sites). Route failures through common cleanup or add a scope guard that always calls commandStop() once the script has been launched.
Reject submissions when the writer thread is stopped
src/rtde/rtde_writer.cpp:188
stop() leaves these buffers typed, so this path still returns true while no writer thread is running. That is especially misleading during reconnect: the accepted update is never sent, and the next init() clears new_data_available_. Reject stopped-writer submissions before copying the package.
…ic_t in round-trip example
- RTDEWriter::init() throws if setRecipeTypes() has not typed the input buffers.
- sendPackage() and the send* helpers return false while the writer is not running
instead of silently staging data that would be sent after the next init().
- rtde_roundtrip example uses volatile std::sig_atomic_t for the SIGINT flag and
drops the g_ prefix.
Sends could observe running_ before new_data_available_ was cleared and the
writer thread existed, so an accepted update could lose its notification.
Reset the flag and create the thread while holding the mutex, and roll
running_ back if thread creation throws.
The reason will be displayed to describe this comment to others. Learn more.
This is looking good now. I verified that parse times aren't negatively affected by the changes made here and I ran some manual tests additionally to reviewing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replace the hardcoded RTDE field-name/type table with types reported by the robot during recipe setup. This allows applications to use additional controller fields without updating the library's field table, provided their protocol types are supported.
Changes
DataPackage::getDataType()and their protocol names throughtoString(DataType).RTDEClient::createInputDataPackage()for pre-typed input packages and validate outgoing packages against the negotiated recipe. Unset input fields in matching recipes are sent as typed zeros.TCPServer::writeUnchecked().Compatibility
RTDEInvalidKeyExceptionduringRTDEClient::init(), rather than construction.ignore_unavailable_outputsalso filters unknown names.setData()are checked against the robot when sent; pre-typed input packages reject mismatches immediately.getData()still throwsstd::bad_variant_accessfor a present field with the wrong or unset type.std::stringis no longer aDataPackagevariant alternative, so using it withgetData()orsetData()is a compile error.getOutputRecipe()for the allocation-free path.See doc/migration_notes.rst for migration details.